﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Simulator
{
	public partial class LoggerArea : UserControl
	{
		public struct Entry
		{
			public readonly int PortIndex;
			public readonly int TreeIndex;
			public readonly string Text;

			public Entry (int portIndex, int treeIndex, string text)
			{
				this.PortIndex = portIndex;
				this.TreeIndex = treeIndex;
				this.Text = text;
			}
		}

		// Most variables need to be protected with lock(_loggerLock) because they might be changed
		// by the GUI thread while being read by the thread that adds text.

		object _loggerLock;
		System.Threading.Timer _timer;
		Dictionary<Bridge, List<Entry>> _entriesPerBridge;
		Bridge _selectedBridge;
		int _selectedPortIndex;
		int _selectedTreeIndex;
		//int _tabSize;
		Brush _backColorBrush;
		Size? _charSize;
		Size _clientSize;
		int _maxNumberOfVisibleLines;

		List<StringBuilder> _selectedLines;
		int _currentLineIndex;
		int _currentColIndex;
		int _maxLineWidth;

		static readonly Font _logFont;

		static LoggerArea ()
		{
			_logFont = new Font ("Consolas", 9);
			if (_logFont.OriginalFontName != _logFont.Name)
			{
				_logFont.Dispose ();
				_logFont = new Font ("Courier New", 9);
			}
		}

		public LoggerArea ()
		{
			InitializeComponent ();

			_entriesPerBridge = new Dictionary<Bridge, List<Entry>> ();
			_selectedPortIndex = -1;
			_selectedTreeIndex = -1;
			//_tabSize = 4;

			_loggerLock = new object ();

			_selectedLines = new List<StringBuilder> ();

			_backColorBrush = new SolidBrush (this.BackColor);

			_timer = new System.Threading.Timer (TimerCallback);

			this.SetStyle (ControlStyles.AllPaintingInWmPaint, true);
			this.SetStyle (ControlStyles.OptimizedDoubleBuffer, true);

			this.AutoScroll = true;
		}

		void TimerCallback (object param)
		{
			var del = new MethodInvoker (() => 
				{
					lock (_loggerLock)
						UpdateScrollBars ();

					Invalidate ();
				});

			BeginInvoke (del);
		}

		void AddEntryToSelectedLines (Entry entry)
		{
			foreach (var ch in entry.Text)
			{
				if (ch == '\x0d')
				{
					_currentColIndex = 0;
				}
				else if (ch == '\x0a')
				{
					_currentLineIndex++;
					if (_currentLineIndex == _selectedLines.Count)
						_selectedLines.Add (new StringBuilder ());
				}
				else if (ch == '\x08')
				{
					// backspace
					if (_currentColIndex > 0)
						_currentColIndex--;
					else
						throw new NotImplementedException ();
				}
				else if (ch == '\x09')
				{
					// tab
					throw new NotImplementedException ();
				}
				else
				{
					if (_currentLineIndex == _selectedLines.Count)
						_selectedLines.Add (new StringBuilder ());
					else if (_currentLineIndex > _selectedLines.Count)
						throw new NotImplementedException ();

					var sb = _selectedLines [_currentLineIndex];
					if (_currentColIndex < sb.Length)
					{
						sb [_currentColIndex] = ch;
					}
					else
					{
						if (_currentColIndex > sb.Length)
							sb.Append (' ', _currentColIndex - sb.Length);

						sb.Append (ch);

						if (sb.Length > _maxLineWidth)
							_maxLineWidth = sb.Length;
					}

					_currentColIndex++;
				}
			}
		}

		bool IsEntrySelected (Entry entry)
		{
			return ((entry.PortIndex == -1) || (_selectedPortIndex == -1) || (entry.PortIndex == _selectedPortIndex))
				&& ((entry.TreeIndex == -1) || (_selectedTreeIndex == -1) || (entry.TreeIndex == _selectedTreeIndex));
		}

		internal void AppendText (Bridge bridge, int portIndex, int treeIndex, string text)
		{
			// We don't know which thread this is called from. Could be called from the interface thread or from a Bridge's thread.

			lock (_loggerLock)
			{
				List<Entry> entries;
				if (_entriesPerBridge.TryGetValue (bridge, out entries) == false)
				{
					entries = new List<Entry> ();
					_entriesPerBridge.Add (bridge, entries);
				}

				Entry entry = new Entry (portIndex, treeIndex, text);
				entries.Add (entry);

				if ((bridge == _selectedBridge) && IsEntrySelected (entry))
				{
					AddEntryToSelectedLines (entry);

					_timer.Change (50, 0);
				}
			}
		}

		public void Select (Bridge bridge, int portIndex, int treeIndex)
		{
			Debug.Assert (InvokeRequired == false);

			lock (_loggerLock)
			{
				if ((_selectedBridge == bridge) && (_selectedPortIndex == portIndex) && (_selectedTreeIndex == treeIndex))
					return;

				_selectedBridge = bridge;
				_selectedPortIndex = portIndex;
				_selectedTreeIndex = treeIndex;
				_selectedLines.Clear ();
				_currentLineIndex = 0;
				_currentColIndex = 0;
				_maxLineWidth = 0;

				if (bridge != null)
				{
					if (_entriesPerBridge.ContainsKey (bridge) == false)
						_entriesPerBridge.Add (bridge, new List<Entry> ());

					foreach (var entry in _entriesPerBridge [bridge])
					{
						if (IsEntrySelected (entry))
						{
							AddEntryToSelectedLines (entry);
						}
					}
				}

				UpdateScrollBars ();
			}

			Invalidate ();
		}

		void UpdateScrollBars ()
		{
			Debug.Assert (InvokeRequired == false);

			bool scrolledToEnd = (-AutoScrollPosition.Y + CharSize.Height * _maxNumberOfVisibleLines >= AutoScrollMinSize.Height);

			//CalculateAutoScrollMinSize ();
			int textHeight = CharSize.Height * _selectedLines.Count;
			int textWidth = CharSize.Width * _maxLineWidth;
			this.AutoScrollMinSize = new Size (textWidth, textHeight);

			if (scrolledToEnd)
			{
				// scroll to the new end
				if (textHeight <= _clientSize.Height)
					this.AutoScrollPosition = new Point (0, 0);
				else
					this.AutoScrollPosition = new Point (0, textHeight - _clientSize.Height);
			}
		}
		
		protected override void OnClientSizeChanged (EventArgs e)
		{
			base.OnClientSizeChanged (e);

			lock (_loggerLock)
			{
				_clientSize = ClientSize;

				_maxNumberOfVisibleLines = (int) Math.Ceiling ((double) ClientSize.Height / (double) CharSize.Height);
			}
		}

		protected override void OnBackColorChanged (EventArgs e)
		{
			base.OnBackColorChanged (e);

			if (_backColorBrush != null)
				_backColorBrush.Dispose ();

			_backColorBrush = new SolidBrush (this.BackColor);
		}

		Size CharSize
		{
			get
			{
				if (_charSize == null)
				{
					using (var g = this.CreateGraphics ())
					{
						Size proposedSize = new Size (Int32.MaxValue, Int32.MaxValue);
						_charSize = TextRenderer.MeasureText (g, "A", _logFont, proposedSize, TextFormatFlags.NoPadding);
					}
				}

				return _charSize.Value;
			}
		}

		protected override void OnPaint (PaintEventArgs e)
		{
			base.OnPaint (e);

			if (DesignMode)
				return;

			e.Graphics.FillRectangle (_backColorBrush, e.ClipRectangle);

			if (_selectedBridge == null)
				return;

			lock (_loggerLock)
			{
				int maxNumberOfVisibleLines = (int) Math.Ceiling ((double) _clientSize.Height / (double) CharSize.Height);
				Debug.Assert (maxNumberOfVisibleLines > 0); // this is zero only when _clientSize.Height is zero, but this function shouldn't be called in this case.

				int x = AutoScrollPosition.X;
				int y = AutoScrollPosition.Y;
				foreach (var line in _selectedLines)
				{
					Rectangle lineRect = new Rectangle (x, y, line.Length * (int) CharSize.Width, (int) CharSize.Height);
					if (e.ClipRectangle.IntersectsWith (lineRect))
					{
						Point p = new Point (x, y);
						TextRenderer.DrawText (e.Graphics, line.ToString (), _logFont, p, this.ForeColor, Color.Transparent, TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix);
					}

					y += (int) CharSize.Height;
				}
			}
		}

		internal void Clear ()
		{
			lock (_loggerLock)
			{
				_entriesPerBridge.Clear ();
				_selectedLines.Clear ();
				_currentLineIndex = 0;
				_currentColIndex = 0;
				_maxLineWidth = 0;

				UpdateScrollBars ();
			}

			Invalidate ();
		}

		internal void CopyAll ()
		{
			Clipboard.SetText (String.Join ("\r\n", _selectedLines), TextDataFormat.UnicodeText);
		}

		[Browsable (false)]
		public Bridge SelectedBridge
		{
			get { return _selectedBridge; }
		}

		[Browsable (false)]
		public int SelectedPortIndex
		{
			get { return _selectedPortIndex; }
		}

		[Browsable (false)]
		public int SelectedTreeIndex
		{
			get { return _selectedTreeIndex; }
		}

		[Browsable (false)]
		public IEnumerable<KeyValuePair<Bridge, List<Entry>>> EntriesPerBridge
		{
			get { return _entriesPerBridge; }
		}
	}
}
